//Copyright 2010 Balazs Keri, Imre Keri
//
//This file is part of FireworksCreator.
//
//FireworksCreator is free software: you can redistribute it and/or modify
//it under the terms of the GNU General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//
//FireworksCreator is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.
//
//You should have received a copy of the GNU General Public License
//along with FireworksCreator.  If not, see <http://www.gnu.org/licenses/>.
//
#include "particlesystem.h"
#include <cmath>
#include <osg/Matrix>
#include <osg/GL>
#include <osg/StateSet>
#include <osg/Texture2D>
#include <osg/BlendFunc>
#include <osg/TexEnv>
#include <osg/Material>
#include <osg/Notify>
#include <osg/io_utils>
#include <osgDB/ReadFile>
#include <model/particletemplate.h>
#include "particlefactory.h"
#include "controller.h"
#include "particle.h"
//
namespace player
{
using namespace model;
ParticleSystem::ParticleSystem()
:
   osg::Drawable(),
   controller(0),
   particle_factory(0),
   particle_template(0),
   model_particle_template(0),
   particles_visible(false),
   gravitation_enabled(false),
   friction_enabled(false),
   particle_shape(ParticleTemplate::shape_point),
   particle_density(0),
   particle_tile_s(0),
   particle_tile_t(0),
   particle_s_tile(0),
   particle_t_tile(0),
   particle_num_tile(0),
   particle_tile_speed_mode(ParticleTemplate::tile_speed_relative),
   alignment(alignment_billboard),
   align_x_axis(1,0,0),
   align_y_axis(0,1,0),
   particle_scale_reference_frame(particle_scale_world_coordinates),
   default_bounding_box(osg::Vec3(-1,-1,-1),osg::Vec3(1,1,1)),
   doublepass(false),
   bounds_min(0,0,0),
   bounds_max(0,0,0),
   reset_bounds_flag(false),
   bounds_computed(false),
   last_frame(0),
   draw_count(0)
{
   // we don't support display lists because particle systems
   // are dynamic, and they always changes between frames
   setSupportsDisplayList(false);
}
ParticleSystem::ParticleSystem(const ParticleSystem &p_other,const osg::CopyOp &p_copyop)
:
   osg::Drawable(p_other,p_copyop),
   controller(0),
   particle_factory(0),
   particle_template(0),
   model_particle_template(0),
   particles_visible(false),
   gravitation_enabled(false),
   friction_enabled(false),
   particle_shape(ParticleTemplate::shape_point),
   particle_density(0),
   particle_tile_s(0),
   particle_tile_t(0),
   particle_s_tile(0),
   particle_t_tile(0),
   particle_num_tile(0),
   particle_tile_speed_mode(ParticleTemplate::tile_speed_relative),
   alignment(alignment_billboard),
   align_x_axis(1,0,0),
   align_y_axis(0,1,0),
   particle_scale_reference_frame(particle_scale_world_coordinates),
   default_bounding_box(osg::Vec3(-1,-1,-1),osg::Vec3(1,1,1)),
   doublepass(false),
   bounds_min(0,0,0),
   bounds_max(0,0,0),
   reset_bounds_flag(false),
   bounds_computed(false),
   last_frame(0),
   draw_count(0)
{
   setSupportsDisplayList(false);
}
ParticleSystem::~ParticleSystem()
{
   delete particle_template;
   qDebug()<<"Player::ParticleSystem::~ParticleSystem";
}
ParticleSystem &ParticleSystem::operator=(const ParticleSystem &p_other)
{
   osg::Drawable::operator=(p_other);
   if(&p_other!=this)
   {
      deassign_controller();
   }
   return *this;
}
void ParticleSystem::assign_controller(Controller *p_controller,ParticleFactory *p_particle_factory,const model::ParticleTemplate *p_model_particle_template)
{
   if(controller!=0)
      deassign_controller();
   //
   controller=p_controller;
   particle_factory=p_particle_factory;
   model_particle_template=p_model_particle_template;
   //
   particle_template=particle_factory->create();
   //
   particles_visible=model_particle_template->get_visible();
   set_particle_texture_tile(model_particle_template->get_tile_s(),model_particle_template->get_tile_t(),model_particle_template->get_num_tiles());
   particle_tile_speed_mode=model_particle_template->get_tile_speed_mode();
   particle_shape=model_particle_template->get_shape();
   particle_density=model_particle_template->get_density();
   gravitation_enabled=model_particle_template->get_gravitation_enabled()&&controller->get_gravitation_enabled();
   friction_enabled=model_particle_template->get_friction_enabled()&&controller->get_friction_enabled();
   set_default_attributes(model_particle_template->get_texture_file().toStdString(),true,false,0);
}
void ParticleSystem::deassign_controller()
{
   particles.clear();
   while(!dead_particles.empty())
      dead_particles.pop();
   create_queue.clear();
   delete particle_template;particle_template=0;
   controller=0;
   particle_factory=0;
   model_particle_template=0;
   //
   reset_bounds_flag=false;
   bounds_computed=false;
   last_frame=0;
   draw_count=0;
}
/*void ParticleSystem::update_from_controller()
{
   if(controller)
   {
      gravitation_enabled=model_particle_template->get_gravitation_enabled()&&controller->get_gravitation_enabled();
      friction_enabled=model_particle_template->get_friction_enabled()&&controller->get_friction_enabled();
   }
}*/
void ParticleSystem::update_from_particle_template()
{
   if(controller)
   {
      particles_visible=model_particle_template->get_visible();
      set_particle_texture_tile(model_particle_template->get_tile_s(),model_particle_template->get_tile_t(),model_particle_template->get_num_tiles());
      particle_tile_speed_mode=model_particle_template->get_tile_speed_mode();
      particle_shape=model_particle_template->get_shape();
      particle_density=model_particle_template->get_density();
      //
      gravitation_enabled=model_particle_template->get_gravitation_enabled()&&controller->get_gravitation_enabled();
      friction_enabled=model_particle_template->get_friction_enabled()&&controller->get_friction_enabled();
      //
      set_default_attributes(model_particle_template->get_texture_file().toStdString(),true,false,0);
   }
}
void ParticleSystem::update(double p_dt)
{
   if(!controller)
      return;
   // reset bounds
   reset_bounds_flag=true;
   //
   // update particles
   for(unsigned int i=0;i<particles.size();++i)
   {
      Particle &particle=particles[i];
      if(particle.is_alive())
      {
         if(particle.update(p_dt))
            update_bounds(particle.get_position(),particle.get_current_size());
         else
            reuse_particle(i);
      }
   }
   //
   // add particles
   while(!create_queue.empty())
   {
      int i=new_particle();
      Particle &particle=particles[i];
      particle_factory->setup(&particle,create_queue.front());
      particle.start();
      if(particle.update(controller->get_current_time()-create_queue.front().time))
         update_bounds(particle.get_position(),particle.get_current_size());
      else
         reuse_particle(i);
      create_queue.pop_front();
   }
   // force recomputing of bounding box on next frame
   dirtyBound();
}
void ParticleSystem::reset()
{
   particles.clear();
   while(!dead_particles.empty())
      dead_particles.pop();
   create_queue.clear();
   //other to do?
}
void ParticleSystem::drawImplementation(osg::RenderInfo &renderInfo)const
{
   if(!controller)
      return;
   //
   osg::State &state=*renderInfo.getState();
   //
   ScopedReadLock lock(read_write_mutex);
   //
   // update the frame count, so other objects can detect when
   // this particle system is culled
   last_frame=state.getFrameStamp()->getFrameNumber();
   //
   // get the current modelview matrix
   osg::Matrix modelview=state.getModelViewMatrix();
   //
   // set up depth mask for first rendering pass
   glPushAttrib(GL_DEPTH_BUFFER_BIT);
   glDepthMask(GL_FALSE);
   //
   // render, first pass
   single_pass_render(state, modelview);
   //
   // restore depth mask settings
   glPopAttrib();
   //
   // render, second pass
   if(doublepass)
   {
      // set up color mask for second rendering pass
      glPushAttrib(GL_COLOR_BUFFER_BIT);
      glColorMask(GL_FALSE,GL_FALSE,GL_FALSE,GL_FALSE);
      // render the particles onto the depth buffer
      single_pass_render(state,modelview);
      // restore color mask settings
      glPopAttrib();
   }
}
void ParticleSystem::update_gravitation_enabled()
{
   if(controller)
      gravitation_enabled=model_particle_template->get_gravitation_enabled()&&controller->get_gravitation_enabled();
}
void ParticleSystem::update_friction_enabled()
{
   if(controller)
      friction_enabled=model_particle_template->get_friction_enabled()&&controller->get_friction_enabled();
}
void ParticleSystem::set_particle_texture_tile(int p_tile_s,int p_tile_t,float p_num_tiles)
{
   particle_tile_s=p_tile_s;
   particle_tile_t=p_tile_t;
   particle_s_tile=(p_tile_s>0)?1.0f/static_cast<float>(p_tile_s):1.0f;
   particle_t_tile=(p_tile_t>0)?1.0f/static_cast<float>(p_tile_t):1.0f;
   if(p_num_tiles==0)
      particle_num_tile=p_tile_s*p_tile_t;
   else
      particle_num_tile=p_num_tiles;
}
void ParticleSystem::set_default_attributes(const std::string &texturefile,bool emissive_particles,bool lighting,int texture_unit)
{
   osg::StateSet *stateset=new osg::StateSet;
   //
   stateset->setMode(GL_LIGHTING,lighting?osg::StateAttribute::ON:osg::StateAttribute::OFF);
   stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
   //
   osg::Material *material=new osg::Material;
   material->setSpecular(osg::Material::FRONT,osg::Vec4(0,0,0,1));
   material->setEmission(osg::Material::FRONT,osg::Vec4(0,0,0,1));
   material->setColorMode(lighting?osg::Material::AMBIENT_AND_DIFFUSE:osg::Material::OFF);
   stateset->setAttributeAndModes(material,osg::StateAttribute::ON);
   //
   if(!texturefile.empty())
   {
      osg::Texture2D *texture=new osg::Texture2D;
      texture->setImage(osgDB::readImageFile(texturefile));
      texture->setFilter(osg::Texture2D::MIN_FILTER, osg::Texture2D::LINEAR);
      texture->setFilter(osg::Texture2D::MAG_FILTER, osg::Texture2D::LINEAR);
      texture->setWrap(osg::Texture2D::WRAP_S, osg::Texture2D::MIRROR);
      texture->setWrap(osg::Texture2D::WRAP_T, osg::Texture2D::MIRROR);
      stateset->setTextureAttributeAndModes(texture_unit,texture,osg::StateAttribute::ON);
      osg::TexEnv *texenv=new osg::TexEnv;
      texenv->setMode(osg::TexEnv::MODULATE);
      stateset->setTextureAttribute(texture_unit, texenv);
   }
   //
   osg::BlendFunc *blend=new osg::BlendFunc;
   if(emissive_particles)
      blend->setFunction(osg::BlendFunc::SRC_ALPHA, osg::BlendFunc::ONE);
   else
      blend->setFunction(osg::BlendFunc::SRC_ALPHA, osg::BlendFunc::ONE_MINUS_SRC_ALPHA);
   stateset->setAttributeAndModes(blend,osg::StateAttribute::ON);
   //
   setStateSet(stateset);
}
void ParticleSystem::single_pass_render(osg::State &/*state*/,const osg::Matrix &modelview)const
{
   int _detail=1;
   //
   draw_count=0;
   if(particles.size()<=0)
      return;
   //
   float scale=std::sqrt(static_cast<float>(_detail));
   osg::Vec3 xAxis=align_x_axis;
   osg::Vec3 yAxis=align_y_axis;
   osg::Vec3 scaled_aligned_xAxis=align_x_axis;
   osg::Vec3 scaled_aligned_yAxis=align_y_axis;
   float xScale=1.0f;
   float yScale=1.0f;
   //
   if(alignment==alignment_billboard)
   {
      xAxis=osg::Matrix::transform3x3(modelview,align_x_axis);
      yAxis=osg::Matrix::transform3x3(modelview,align_y_axis);
      float lengthX2=xAxis.length2();
      float lengthY2=yAxis.length2();
      if(particle_scale_reference_frame==particle_scale_local_coordinates)
      {
         xScale = 1.0f/std::sqrt(lengthX2);
         yScale = 1.0f/std::sqrt(lengthY2);
      }
      else
      {
         xScale=1.0f/lengthX2;
         yScale=1.0f/lengthY2;
      }
      scaled_aligned_xAxis*=xScale;
      scaled_aligned_yAxis*=yScale;
      xAxis*=xScale;
      yAxis*=yScale;
   }
   //
   // all particles are of the same kind from "rendering point of view" (shape is the same) as the template
   // so begin_render and end_render is called on the template
   for(unsigned int i=0;i<particles.size();i+=_detail)
   {
      const Particle &currentParticle=particles[i];
      if(currentParticle.is_alive())
      {
         if(draw_count==0)
            particle_template->begin_render();
         ++draw_count;
         //
         /*const osg::Vec3 &angle=currentParticle->get_angle();
         bool requiresRotation=(angle.x()!=0.0f||angle.y()!=0.0f||angle.z()!=0.0f);
         if(requiresRotation)
         {
            osg::Matrix R;
            R.makeRotate(
               angle.x(),osg::Vec3(1,0,0),
               angle.y(),osg::Vec3(0,1,0),
               angle.z(),osg::Vec3(0,0,1)
            );
            xAxis=osg::Matrix::transform3x3(R,scaled_aligned_xAxis);
            yAxis=osg::Matrix::transform3x3(R,scaled_aligned_yAxis);
            if(alignment==alignment_billboard)
            {
               xAxis=osg::Matrix::transform3x3(modelview,xAxis);
               yAxis=osg::Matrix::transform3x3(modelview,yAxis);
            }
         }*/
         currentParticle.render(currentParticle.get_position(),xAxis,yAxis,scale);
      }
   }
   if(draw_count>0)
      particle_template->end_render();
}
osg::BoundingBox ParticleSystem::computeBound()const
{
   if(!bounds_computed)
   {
      return default_bounding_box;
   }
   else
   {
      return osg::BoundingBox(bounds_min,bounds_max);
   }
}
int ParticleSystem::new_particle()
{
   if(!dead_particles.empty())
   {
      int i=dead_particles.top();
      dead_particles.pop();
      return i;
   }
   else
   {
      // add a new particle to the vector
      // copy it from the template
      particles.push_back(*particle_template);
      //particles.back().set_particle_system(this);//or set in particle_template?
      // return index of the new (last) particle
      return particles.size()-1;
   }
}
}
